migrate to monorepo?
This commit is contained in:
21
packages/mcp-server/package.json
Normal file
21
packages/mcp-server/package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "@marketplace-scrapers/mcp-server",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"module": "./src/index.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "bun ./src/index.ts",
|
||||
"dev": "bun --watch ./src/index.ts",
|
||||
"build": "bun build ./src/index.ts --target=bun --outdir=../../dist/mcp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@marketplace-scrapers/core": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
33
packages/mcp-server/src/index.ts
Normal file
33
packages/mcp-server/src/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { handleMcpRequest } from "./protocol/handler";
|
||||
import { serverCard } from "./protocol/metadata";
|
||||
|
||||
const PORT = process.env.MCP_PORT || 4006;
|
||||
|
||||
const server = Bun.serve({
|
||||
port: PORT as number | string,
|
||||
idleTimeout: 0,
|
||||
routes: {
|
||||
// MCP metadata discovery endpoint
|
||||
"/.well-known/mcp/server-card.json": new Response(JSON.stringify(serverCard), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
|
||||
// MCP JSON-RPC 2.0 protocol endpoint
|
||||
"/mcp": async (req: Request) => {
|
||||
if (req.method === "POST") {
|
||||
return await handleMcpRequest(req);
|
||||
}
|
||||
return Response.json(
|
||||
{ message: "MCP endpoint requires POST request" },
|
||||
{ status: 405 }
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// Fallback for all other routes
|
||||
fetch(req: Request) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`MCP Server running on ${server.hostname}:${server.port}`);
|
||||
185
packages/mcp-server/src/protocol/handler.ts
Normal file
185
packages/mcp-server/src/protocol/handler.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { fetchKijijiItems, fetchFacebookItems, fetchEbayItems } from "@marketplace-scrapers/core";
|
||||
import { tools } from "./tools";
|
||||
|
||||
/**
|
||||
* Handle MCP JSON-RPC 2.0 protocol requests
|
||||
*/
|
||||
export async function handleMcpRequest(req: Request): Promise<Response> {
|
||||
try {
|
||||
const body = await req.json();
|
||||
|
||||
// Validate JSON-RPC 2.0 format
|
||||
if (!body.jsonrpc || body.jsonrpc !== "2.0" || !body.method) {
|
||||
return Response.json(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32600, message: "Invalid Request" },
|
||||
id: body.id,
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { method, params, id } = body;
|
||||
|
||||
// Handle initialize method
|
||||
if (method === "initialize") {
|
||||
return Response.json({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
protocolVersion: "2025-06-18",
|
||||
capabilities: {
|
||||
tools: {
|
||||
listChanged: true,
|
||||
},
|
||||
},
|
||||
serverInfo: {
|
||||
name: "marketplace-scrapers",
|
||||
version: "1.0.0",
|
||||
},
|
||||
instructions: "Use search_kijiji, search_facebook, or search_ebay tools to find listings across Canadian marketplaces",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Handle tools/list method
|
||||
if (method === "tools/list") {
|
||||
return Response.json({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
tools,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Handle notifications (messages without id field should not get a response)
|
||||
if (!id) {
|
||||
// Notifications don't require a response
|
||||
if (method === "notifications/initialized") {
|
||||
// Client initialized successfully, no response needed
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
if (method === "notifications/progress") {
|
||||
// Progress notifications, no response needed
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
// Unknown notification - still no response for notifications
|
||||
return new Response(null, { status: 204 });
|
||||
}
|
||||
|
||||
// Handle tools/call method
|
||||
if (method === "tools/call") {
|
||||
const { name, arguments: args } = params || {};
|
||||
|
||||
if (!name || !args) {
|
||||
return Response.json(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32602, message: "Invalid params: name and arguments required" },
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Route tool calls to appropriate handlers
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (name === "search_kijiji") {
|
||||
const query = args.query;
|
||||
if (!query) {
|
||||
return Response.json({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32602, message: "query parameter is required" },
|
||||
});
|
||||
}
|
||||
const items = await fetchKijijiItems(query, args.maxItems || 5);
|
||||
result = items || [];
|
||||
} else if (name === "search_facebook") {
|
||||
const query = args.query;
|
||||
if (!query) {
|
||||
return Response.json({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32602, message: "query parameter is required" },
|
||||
});
|
||||
}
|
||||
const items = await fetchFacebookItems(
|
||||
query,
|
||||
args.maxItems || 5,
|
||||
args.location || "toronto",
|
||||
25,
|
||||
args.cookiesSource
|
||||
);
|
||||
result = items || [];
|
||||
} else if (name === "search_ebay") {
|
||||
const query = args.query;
|
||||
if (!query) {
|
||||
return Response.json({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32602, message: "query parameter is required" },
|
||||
});
|
||||
}
|
||||
const items = await fetchEbayItems(query, args.maxItems || 5, {
|
||||
minPrice: args.minPrice,
|
||||
maxPrice: args.maxPrice,
|
||||
strictMode: args.strictMode || false,
|
||||
exclusions: args.exclusions || [],
|
||||
keywords: args.keywords || [query],
|
||||
});
|
||||
result = items || [];
|
||||
} else {
|
||||
return Response.json({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32601, message: `Unknown tool: ${name}` },
|
||||
});
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
result: {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
return Response.json({
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32603, message: `Tool execution failed: ${errorMessage}` },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method not found
|
||||
return Response.json(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id,
|
||||
error: { code: -32601, message: `Method not found: ${method}` },
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
||||
return Response.json(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32700, message: `Parse error: ${errorMessage}` },
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
25
packages/mcp-server/src/protocol/metadata.ts
Normal file
25
packages/mcp-server/src/protocol/metadata.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* MCP Server metadata for discovery
|
||||
*/
|
||||
|
||||
export const serverCard = {
|
||||
$schema: "https://static.modelcontextprotocol.io/schemas/mcp-server-card/v1.json",
|
||||
version: "1.0",
|
||||
protocolVersion: "2025-06-18",
|
||||
serverInfo: {
|
||||
name: "marketplace-scrapers",
|
||||
title: "Marketplace Scrapers MCP Server",
|
||||
version: "1.0.0",
|
||||
},
|
||||
transport: {
|
||||
type: "streamable-http",
|
||||
endpoint: "/mcp",
|
||||
},
|
||||
capabilities: {
|
||||
tools: {
|
||||
listChanged: true,
|
||||
},
|
||||
},
|
||||
description: "Scrapes marketplace listings from Kijiji, Facebook Marketplace, and eBay",
|
||||
tools: "dynamic",
|
||||
};
|
||||
95
packages/mcp-server/src/protocol/tools.ts
Normal file
95
packages/mcp-server/src/protocol/tools.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* MCP tool definitions for marketplace scrapers
|
||||
*/
|
||||
|
||||
export const tools = [
|
||||
{
|
||||
name: "search_kijiji",
|
||||
description: "Search Kijiji marketplace for listings matching a query",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query for Kijiji listings",
|
||||
},
|
||||
maxItems: {
|
||||
type: "number",
|
||||
description: "Maximum number of items to return",
|
||||
default: 5,
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "search_facebook",
|
||||
description: "Search Facebook Marketplace for listings matching a query",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query for Facebook Marketplace listings",
|
||||
},
|
||||
location: {
|
||||
type: "string",
|
||||
description: "Location for search (e.g., 'toronto')",
|
||||
default: "toronto",
|
||||
},
|
||||
maxItems: {
|
||||
type: "number",
|
||||
description: "Maximum number of items to return",
|
||||
default: 5,
|
||||
},
|
||||
cookiesSource: {
|
||||
type: "string",
|
||||
description: "Optional Facebook session cookies source",
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "search_ebay",
|
||||
description: "Search eBay for listings matching a query",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
query: {
|
||||
type: "string",
|
||||
description: "Search query for eBay listings",
|
||||
},
|
||||
minPrice: {
|
||||
type: "number",
|
||||
description: "Minimum price filter",
|
||||
},
|
||||
maxPrice: {
|
||||
type: "number",
|
||||
description: "Maximum price filter",
|
||||
},
|
||||
strictMode: {
|
||||
type: "boolean",
|
||||
description: "Enable strict search mode",
|
||||
default: false,
|
||||
},
|
||||
exclusions: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Terms to exclude from results",
|
||||
},
|
||||
keywords: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Keywords to include in search",
|
||||
},
|
||||
maxItems: {
|
||||
type: "number",
|
||||
description: "Maximum number of items to return",
|
||||
default: 5,
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
},
|
||||
];
|
||||
13
packages/mcp-server/tsconfig.json
Normal file
13
packages/mcp-server/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user