|
@@ -1,11 +1,12 @@
|
|
|
/**
|
|
/**
|
|
|
- * MCP HTTP Server for WooCommerce
|
|
|
|
|
|
|
+ * MCP SSE Server for WooCommerce
|
|
|
*
|
|
*
|
|
|
* Provides MCP tools for accessing WooCommerce orders and customers data
|
|
* Provides MCP tools for accessing WooCommerce orders and customers data
|
|
|
- * without storing personal information locally (GDPR compliance).
|
|
|
|
|
|
|
+ * using Server-Sent Events (SSE) protocol for n8n integration.
|
|
|
*
|
|
*
|
|
|
* Authentication: Internal API keys from internal_api_keys table
|
|
* Authentication: Internal API keys from internal_api_keys table
|
|
|
* Required parameter: shop_id (store ID from stores table)
|
|
* Required parameter: shop_id (store ID from stores table)
|
|
|
|
|
+ * Protocol: MCP over HTTP with SSE (2024-11-05)
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
|
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
|
@@ -21,8 +22,7 @@ import {
|
|
|
WooCommerceCustomer
|
|
WooCommerceCustomer
|
|
|
} from '../_shared/woocommerce-client.ts';
|
|
} from '../_shared/woocommerce-client.ts';
|
|
|
import {
|
|
import {
|
|
|
- McpToolsListResponse,
|
|
|
|
|
- McpToolCallRequest,
|
|
|
|
|
|
|
+ McpTool,
|
|
|
LlmCustomer,
|
|
LlmCustomer,
|
|
|
LlmOrder
|
|
LlmOrder
|
|
|
} from '../_shared/mcp-types.ts';
|
|
} from '../_shared/mcp-types.ts';
|
|
@@ -31,83 +31,99 @@ import {
|
|
|
createMcpSuccessResponse,
|
|
createMcpSuccessResponse,
|
|
|
validateParams
|
|
validateParams
|
|
|
} from '../_shared/mcp-helpers.ts';
|
|
} from '../_shared/mcp-helpers.ts';
|
|
|
|
|
+import {
|
|
|
|
|
+ JsonRpcRequest,
|
|
|
|
|
+ createSseResponse,
|
|
|
|
|
+ sendSseMessage,
|
|
|
|
|
+ closeSseStream,
|
|
|
|
|
+ parseJsonRpcRequest,
|
|
|
|
|
+ createJsonRpcSuccess,
|
|
|
|
|
+ createJsonRpcError,
|
|
|
|
|
+ createInitializeResult,
|
|
|
|
|
+ validateInitializeParams,
|
|
|
|
|
+ validateToolCallParams,
|
|
|
|
|
+ ToolCallParams,
|
|
|
|
|
+ ToolCallResult,
|
|
|
|
|
+ JsonRpcErrorCode
|
|
|
|
|
+} from '../_shared/mcp-sse.ts';
|
|
|
|
|
|
|
|
// Initialize Supabase client
|
|
// Initialize Supabase client
|
|
|
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
|
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
|
|
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
|
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
|
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
|
|
|
|
|
|
|
|
+const SERVER_NAME = 'mcp-woocommerce';
|
|
|
|
|
+const SERVER_VERSION = '1.0.0';
|
|
|
|
|
+
|
|
|
// MCP Tool Definitions
|
|
// MCP Tool Definitions
|
|
|
-const TOOLS: McpToolsListResponse = {
|
|
|
|
|
- tools: [
|
|
|
|
|
- {
|
|
|
|
|
- name: 'woocommerce_list_orders',
|
|
|
|
|
- description: 'List orders from a WooCommerce store. Returns order details including customer info, items, status, and totals.',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- shop_id: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'The UUID of the WooCommerce store from the stores table'
|
|
|
|
|
- },
|
|
|
|
|
- status: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'Filter by order status: any, pending, processing, on-hold, completed, cancelled, refunded, failed'
|
|
|
|
|
- },
|
|
|
|
|
- page: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Page number for pagination (default: 1)'
|
|
|
|
|
- },
|
|
|
|
|
- per_page: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Number of orders per page (default: 25, max: 100)'
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const TOOLS: McpTool[] = [
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'woocommerce_list_orders',
|
|
|
|
|
+ description: 'List orders from a WooCommerce store. Returns order details including customer info, items, status, and totals.',
|
|
|
|
|
+ inputSchema: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ shop_id: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ description: 'The UUID of the WooCommerce store from the stores table'
|
|
|
},
|
|
},
|
|
|
- required: ['shop_id']
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'woocommerce_list_customers',
|
|
|
|
|
- description: 'List customers from a WooCommerce store. Returns customer details including contact info, order count, and total spent.',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- shop_id: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'The UUID of the WooCommerce store from the stores table'
|
|
|
|
|
- },
|
|
|
|
|
- page: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Page number for pagination (default: 1)'
|
|
|
|
|
- },
|
|
|
|
|
- per_page: {
|
|
|
|
|
- type: 'number',
|
|
|
|
|
- description: 'Number of customers per page (default: 25, max: 100)'
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ status: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ description: 'Filter by order status: any, pending, processing, on-hold, completed, cancelled, refunded, failed'
|
|
|
},
|
|
},
|
|
|
- required: ['shop_id']
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- name: 'woocommerce_get_customer_orders',
|
|
|
|
|
- description: 'Get all orders for a specific customer by customer email. Returns detailed order history.',
|
|
|
|
|
- inputSchema: {
|
|
|
|
|
- type: 'object',
|
|
|
|
|
- properties: {
|
|
|
|
|
- shop_id: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'The UUID of the WooCommerce store from the stores table'
|
|
|
|
|
- },
|
|
|
|
|
- customer_email: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- description: 'The customer email address'
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ page: {
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ description: 'Page number for pagination (default: 1)'
|
|
|
},
|
|
},
|
|
|
- required: ['shop_id', 'customer_email']
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ per_page: {
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ description: 'Number of orders per page (default: 25, max: 100)'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ required: ['shop_id']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'woocommerce_list_customers',
|
|
|
|
|
+ description: 'List customers from a WooCommerce store. Returns customer details including contact info, order count, and total spent.',
|
|
|
|
|
+ inputSchema: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ shop_id: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ description: 'The UUID of the WooCommerce store from the stores table'
|
|
|
|
|
+ },
|
|
|
|
|
+ page: {
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ description: 'Page number for pagination (default: 1)'
|
|
|
|
|
+ },
|
|
|
|
|
+ per_page: {
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ description: 'Number of customers per page (default: 25, max: 100)'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ required: ['shop_id']
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ name: 'woocommerce_get_customer_orders',
|
|
|
|
|
+ description: 'Get all orders for a specific customer by customer email. Returns detailed order history.',
|
|
|
|
|
+ inputSchema: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ shop_id: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ description: 'The UUID of the WooCommerce store from the stores table'
|
|
|
|
|
+ },
|
|
|
|
|
+ customer_email: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ description: 'The customer email address'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ required: ['shop_id', 'customer_email']
|
|
|
}
|
|
}
|
|
|
- ]
|
|
|
|
|
-};
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+];
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Convert WooCommerce customer to LLM-friendly format
|
|
* Convert WooCommerce customer to LLM-friendly format
|
|
@@ -166,7 +182,7 @@ async function fetchAllOrdersPages(
|
|
|
hasMore = false;
|
|
hasMore = false;
|
|
|
} else {
|
|
} else {
|
|
|
allOrders.push(...orders);
|
|
allOrders.push(...orders);
|
|
|
- hasMore = orders.length === 100; // If we got max results, there might be more
|
|
|
|
|
|
|
+ hasMore = orders.length === 100;
|
|
|
page++;
|
|
page++;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -177,7 +193,7 @@ async function fetchAllOrdersPages(
|
|
|
/**
|
|
/**
|
|
|
* Handle woocommerce_list_orders tool
|
|
* Handle woocommerce_list_orders tool
|
|
|
*/
|
|
*/
|
|
|
-async function handleListOrders(args: Record<string, any>): Promise<Response> {
|
|
|
|
|
|
|
+async function handleListOrders(args: Record<string, any>): Promise<ToolCallResult> {
|
|
|
const { shop_id, status, page = 1, per_page = 25 } = args;
|
|
const { shop_id, status, page = 1, per_page = 25 } = args;
|
|
|
|
|
|
|
|
// Validate shop exists and is WooCommerce
|
|
// Validate shop exists and is WooCommerce
|
|
@@ -189,13 +205,19 @@ async function handleListOrders(args: Record<string, any>): Promise<Response> {
|
|
|
.single();
|
|
.single();
|
|
|
|
|
|
|
|
if (storeError || !store) {
|
|
if (storeError || !store) {
|
|
|
- return createMcpErrorResponse('STORE_NOT_FOUND', 'WooCommerce store not found', 404);
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{ type: 'text', text: JSON.stringify({ error: 'WooCommerce store not found' }) }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Check permissions
|
|
// Check permissions
|
|
|
const permissions = store.data_access_permissions as any;
|
|
const permissions = store.data_access_permissions as any;
|
|
|
if (permissions && !permissions.allow_order_access) {
|
|
if (permissions && !permissions.allow_order_access) {
|
|
|
- return createMcpErrorResponse('PERMISSION_DENIED', 'Order access not allowed for this store', 403);
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{ type: 'text', text: JSON.stringify({ error: 'Order access not allowed for this store' }) }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -205,26 +227,35 @@ async function handleListOrders(args: Record<string, any>): Promise<Response> {
|
|
|
// Format for LLM
|
|
// Format for LLM
|
|
|
const formattedOrders = orders.map(formatOrderForLlm);
|
|
const formattedOrders = orders.map(formatOrderForLlm);
|
|
|
|
|
|
|
|
- return createMcpSuccessResponse({
|
|
|
|
|
- count: formattedOrders.length,
|
|
|
|
|
- page,
|
|
|
|
|
- per_page,
|
|
|
|
|
- orders: formattedOrders
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: JSON.stringify({
|
|
|
|
|
+ count: formattedOrders.length,
|
|
|
|
|
+ page,
|
|
|
|
|
+ per_page,
|
|
|
|
|
+ orders: formattedOrders
|
|
|
|
|
+ })
|
|
|
|
|
+ }]
|
|
|
|
|
+ };
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('[MCP WooCommerce] Error fetching orders:', error);
|
|
console.error('[MCP WooCommerce] Error fetching orders:', error);
|
|
|
- return createMcpErrorResponse(
|
|
|
|
|
- 'FETCH_ERROR',
|
|
|
|
|
- `Failed to fetch orders: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
- 500
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: JSON.stringify({
|
|
|
|
|
+ error: `Failed to fetch orders: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
|
|
|
+ })
|
|
|
|
|
+ }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Handle woocommerce_list_customers tool
|
|
* Handle woocommerce_list_customers tool
|
|
|
*/
|
|
*/
|
|
|
-async function handleListCustomers(args: Record<string, any>): Promise<Response> {
|
|
|
|
|
|
|
+async function handleListCustomers(args: Record<string, any>): Promise<ToolCallResult> {
|
|
|
const { shop_id, page = 1, per_page = 25 } = args;
|
|
const { shop_id, page = 1, per_page = 25 } = args;
|
|
|
|
|
|
|
|
// Validate shop exists and is WooCommerce
|
|
// Validate shop exists and is WooCommerce
|
|
@@ -236,13 +267,19 @@ async function handleListCustomers(args: Record<string, any>): Promise<Response>
|
|
|
.single();
|
|
.single();
|
|
|
|
|
|
|
|
if (storeError || !store) {
|
|
if (storeError || !store) {
|
|
|
- return createMcpErrorResponse('STORE_NOT_FOUND', 'WooCommerce store not found', 404);
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{ type: 'text', text: JSON.stringify({ error: 'WooCommerce store not found' }) }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Check permissions
|
|
// Check permissions
|
|
|
const permissions = store.data_access_permissions as any;
|
|
const permissions = store.data_access_permissions as any;
|
|
|
if (permissions && !permissions.allow_customer_access) {
|
|
if (permissions && !permissions.allow_customer_access) {
|
|
|
- return createMcpErrorResponse('PERMISSION_DENIED', 'Customer access not allowed for this store', 403);
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{ type: 'text', text: JSON.stringify({ error: 'Customer access not allowed for this store' }) }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -252,26 +289,35 @@ async function handleListCustomers(args: Record<string, any>): Promise<Response>
|
|
|
// Format for LLM
|
|
// Format for LLM
|
|
|
const formattedCustomers = customers.map(formatCustomerForLlm);
|
|
const formattedCustomers = customers.map(formatCustomerForLlm);
|
|
|
|
|
|
|
|
- return createMcpSuccessResponse({
|
|
|
|
|
- count: formattedCustomers.length,
|
|
|
|
|
- page,
|
|
|
|
|
- per_page,
|
|
|
|
|
- customers: formattedCustomers
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: JSON.stringify({
|
|
|
|
|
+ count: formattedCustomers.length,
|
|
|
|
|
+ page,
|
|
|
|
|
+ per_page,
|
|
|
|
|
+ customers: formattedCustomers
|
|
|
|
|
+ })
|
|
|
|
|
+ }]
|
|
|
|
|
+ };
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('[MCP WooCommerce] Error fetching customers:', error);
|
|
console.error('[MCP WooCommerce] Error fetching customers:', error);
|
|
|
- return createMcpErrorResponse(
|
|
|
|
|
- 'FETCH_ERROR',
|
|
|
|
|
- `Failed to fetch customers: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
- 500
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: JSON.stringify({
|
|
|
|
|
+ error: `Failed to fetch customers: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
|
|
|
+ })
|
|
|
|
|
+ }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Handle woocommerce_get_customer_orders tool
|
|
* Handle woocommerce_get_customer_orders tool
|
|
|
*/
|
|
*/
|
|
|
-async function handleGetCustomerOrders(args: Record<string, any>): Promise<Response> {
|
|
|
|
|
|
|
+async function handleGetCustomerOrders(args: Record<string, any>): Promise<ToolCallResult> {
|
|
|
const { shop_id, customer_email } = args;
|
|
const { shop_id, customer_email } = args;
|
|
|
|
|
|
|
|
// Validate shop exists and is WooCommerce
|
|
// Validate shop exists and is WooCommerce
|
|
@@ -283,13 +329,19 @@ async function handleGetCustomerOrders(args: Record<string, any>): Promise<Respo
|
|
|
.single();
|
|
.single();
|
|
|
|
|
|
|
|
if (storeError || !store) {
|
|
if (storeError || !store) {
|
|
|
- return createMcpErrorResponse('STORE_NOT_FOUND', 'WooCommerce store not found', 404);
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{ type: 'text', text: JSON.stringify({ error: 'WooCommerce store not found' }) }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Check permissions
|
|
// Check permissions
|
|
|
const permissions = store.data_access_permissions as any;
|
|
const permissions = store.data_access_permissions as any;
|
|
|
if (permissions && (!permissions.allow_order_access || !permissions.allow_customer_access)) {
|
|
if (permissions && (!permissions.allow_order_access || !permissions.allow_customer_access)) {
|
|
|
- return createMcpErrorResponse('PERMISSION_DENIED', 'Order and customer access required', 403);
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{ type: 'text', text: JSON.stringify({ error: 'Order and customer access required' }) }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -302,101 +354,188 @@ async function handleGetCustomerOrders(args: Record<string, any>): Promise<Respo
|
|
|
// Format for LLM
|
|
// Format for LLM
|
|
|
const formattedOrders = customerOrders.map(formatOrderForLlm);
|
|
const formattedOrders = customerOrders.map(formatOrderForLlm);
|
|
|
|
|
|
|
|
- return createMcpSuccessResponse({
|
|
|
|
|
- customerEmail: customer_email,
|
|
|
|
|
- count: formattedOrders.length,
|
|
|
|
|
- orders: formattedOrders
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: JSON.stringify({
|
|
|
|
|
+ customerEmail: customer_email,
|
|
|
|
|
+ count: formattedOrders.length,
|
|
|
|
|
+ orders: formattedOrders
|
|
|
|
|
+ })
|
|
|
|
|
+ }]
|
|
|
|
|
+ };
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('[MCP WooCommerce] Error fetching customer orders:', error);
|
|
console.error('[MCP WooCommerce] Error fetching customer orders:', error);
|
|
|
- return createMcpErrorResponse(
|
|
|
|
|
- 'FETCH_ERROR',
|
|
|
|
|
- `Failed to fetch customer orders: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
|
|
|
- 500
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: JSON.stringify({
|
|
|
|
|
+ error: `Failed to fetch customer orders: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
|
|
|
+ })
|
|
|
|
|
+ }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Handle tool call
|
|
|
|
|
+ */
|
|
|
|
|
+async function handleToolCall(params: ToolCallParams): Promise<ToolCallResult> {
|
|
|
|
|
+ const { name, arguments: args = {} } = params;
|
|
|
|
|
+
|
|
|
|
|
+ // Validate shop_id is provided
|
|
|
|
|
+ const validation = validateParams(args, ['shop_id']);
|
|
|
|
|
+ if (!validation.valid) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: JSON.stringify({
|
|
|
|
|
+ error: `Missing required parameters: ${validation.missing?.join(', ')}`
|
|
|
|
|
+ })
|
|
|
|
|
+ }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Route to appropriate handler
|
|
|
|
|
+ switch (name) {
|
|
|
|
|
+ case 'woocommerce_list_orders':
|
|
|
|
|
+ return await handleListOrders(args);
|
|
|
|
|
+
|
|
|
|
|
+ case 'woocommerce_list_customers':
|
|
|
|
|
+ return await handleListCustomers(args);
|
|
|
|
|
+
|
|
|
|
|
+ case 'woocommerce_get_customer_orders':
|
|
|
|
|
+ const customerValidation = validateParams(args, ['shop_id', 'customer_email']);
|
|
|
|
|
+ if (!customerValidation.valid) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: JSON.stringify({
|
|
|
|
|
+ error: `Missing required parameters: ${customerValidation.missing?.join(', ')}`
|
|
|
|
|
+ })
|
|
|
|
|
+ }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ return await handleGetCustomerOrders(args);
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ return {
|
|
|
|
|
+ content: [{
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ text: JSON.stringify({ error: `Unknown tool: ${name}` })
|
|
|
|
|
+ }],
|
|
|
|
|
+ isError: true
|
|
|
|
|
+ };
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Main request handler
|
|
|
|
|
|
|
+ * Main request handler with MCP SSE protocol
|
|
|
*/
|
|
*/
|
|
|
serve(async (req: Request) => {
|
|
serve(async (req: Request) => {
|
|
|
- // CORS headers
|
|
|
|
|
|
|
+ // CORS preflight
|
|
|
if (req.method === 'OPTIONS') {
|
|
if (req.method === 'OPTIONS') {
|
|
|
return new Response(null, {
|
|
return new Response(null, {
|
|
|
headers: {
|
|
headers: {
|
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Origin': '*',
|
|
|
- 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
|
|
|
|
|
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
|
'Access-Control-Allow-Headers': 'Authorization, Content-Type'
|
|
'Access-Control-Allow-Headers': 'Authorization, Content-Type'
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const url = new URL(req.url);
|
|
|
|
|
-
|
|
|
|
|
- // Handle /tools endpoint - list available tools
|
|
|
|
|
- if (url.pathname.endsWith('/tools') && req.method === 'GET') {
|
|
|
|
|
- return new Response(JSON.stringify(TOOLS), {
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- 'Access-Control-Allow-Origin': '*'
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // MCP SSE endpoint - only POST requests
|
|
|
|
|
+ if (req.method !== 'POST') {
|
|
|
|
|
+ return new Response(JSON.stringify({ error: 'Method not allowed. Use POST.' }), {
|
|
|
|
|
+ status: 405,
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' }
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Handle /call endpoint - execute tool
|
|
|
|
|
- if (url.pathname.endsWith('/call') && req.method === 'POST') {
|
|
|
|
|
- // Authenticate with internal API key
|
|
|
|
|
- const authResult = await requireInternalApiKey(req, supabase, 'read_orders');
|
|
|
|
|
|
|
+ // Authenticate with internal API key
|
|
|
|
|
+ const authResult = await requireInternalApiKey(req, supabase, 'read_orders');
|
|
|
|
|
|
|
|
- if (!authResult.valid) {
|
|
|
|
|
- return createInternalApiKeyErrorResponse(authResult);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (!authResult.valid) {
|
|
|
|
|
+ return createInternalApiKeyErrorResponse(authResult);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- try {
|
|
|
|
|
- const body: McpToolCallRequest = await req.json();
|
|
|
|
|
- const { name, arguments: args } = body;
|
|
|
|
|
|
|
+ // Create SSE response
|
|
|
|
|
+ const { response, controller } = createSseResponse();
|
|
|
|
|
|
|
|
- // Validate shop_id is provided
|
|
|
|
|
- const validation = validateParams(args, ['shop_id']);
|
|
|
|
|
- if (!validation.valid) {
|
|
|
|
|
- return createMcpErrorResponse(
|
|
|
|
|
- 'MISSING_PARAMS',
|
|
|
|
|
- `Missing required parameters: ${validation.missing?.join(', ')}`
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Parse JSON-RPC request
|
|
|
|
|
+ let rpcRequest: JsonRpcRequest;
|
|
|
|
|
+ try {
|
|
|
|
|
+ rpcRequest = await parseJsonRpcRequest(req);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ sendSseMessage(
|
|
|
|
|
+ controller,
|
|
|
|
|
+ createJsonRpcError(
|
|
|
|
|
+ null,
|
|
|
|
|
+ JsonRpcErrorCode.PARSE_ERROR,
|
|
|
|
|
+ error instanceof Error ? error.message : 'Parse error'
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
|
|
+ closeSseStream(controller);
|
|
|
|
|
+ return response;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Route to appropriate handler
|
|
|
|
|
- switch (name) {
|
|
|
|
|
- case 'woocommerce_list_orders':
|
|
|
|
|
- return await handleListOrders(args);
|
|
|
|
|
-
|
|
|
|
|
- case 'woocommerce_list_customers':
|
|
|
|
|
- return await handleListCustomers(args);
|
|
|
|
|
-
|
|
|
|
|
- case 'woocommerce_get_customer_orders':
|
|
|
|
|
- const customerValidation = validateParams(args, ['shop_id', 'customer_email']);
|
|
|
|
|
- if (!customerValidation.valid) {
|
|
|
|
|
- return createMcpErrorResponse(
|
|
|
|
|
- 'MISSING_PARAMS',
|
|
|
|
|
- `Missing required parameters: ${customerValidation.missing?.join(', ')}`
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- return await handleGetCustomerOrders(args);
|
|
|
|
|
-
|
|
|
|
|
- default:
|
|
|
|
|
- return createMcpErrorResponse('UNKNOWN_TOOL', `Unknown tool: ${name}`, 404);
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('[MCP WooCommerce] Error handling request:', error);
|
|
|
|
|
- return createMcpErrorResponse(
|
|
|
|
|
- 'INTERNAL_ERROR',
|
|
|
|
|
- error instanceof Error ? error.message : 'Internal server error',
|
|
|
|
|
- 500
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ const { id, method, params } = rpcRequest;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Handle MCP methods
|
|
|
|
|
+ switch (method) {
|
|
|
|
|
+ case 'initialize':
|
|
|
|
|
+ if (!validateInitializeParams(params)) {
|
|
|
|
|
+ sendSseMessage(
|
|
|
|
|
+ controller,
|
|
|
|
|
+ createJsonRpcError(id!, JsonRpcErrorCode.INVALID_PARAMS, 'Invalid initialize parameters')
|
|
|
|
|
+ );
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const initResult = createInitializeResult(SERVER_NAME, SERVER_VERSION);
|
|
|
|
|
+ sendSseMessage(controller, createJsonRpcSuccess(id!, initResult));
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case 'tools/list':
|
|
|
|
|
+ sendSseMessage(controller, createJsonRpcSuccess(id!, { tools: TOOLS }));
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ case 'tools/call':
|
|
|
|
|
+ if (!validateToolCallParams(params)) {
|
|
|
|
|
+ sendSseMessage(
|
|
|
|
|
+ controller,
|
|
|
|
|
+ createJsonRpcError(id!, JsonRpcErrorCode.INVALID_PARAMS, 'Invalid tool call parameters')
|
|
|
|
|
+ );
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const toolResult = await handleToolCall(params);
|
|
|
|
|
+ sendSseMessage(controller, createJsonRpcSuccess(id!, toolResult));
|
|
|
|
|
+ break;
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ sendSseMessage(
|
|
|
|
|
+ controller,
|
|
|
|
|
+ createJsonRpcError(id!, JsonRpcErrorCode.METHOD_NOT_FOUND, `Method not found: ${method}`)
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('[MCP WooCommerce] Error handling request:', error);
|
|
|
|
|
+ sendSseMessage(
|
|
|
|
|
+ controller,
|
|
|
|
|
+ createJsonRpcError(
|
|
|
|
|
+ id!,
|
|
|
|
|
+ JsonRpcErrorCode.INTERNAL_ERROR,
|
|
|
|
|
+ error instanceof Error ? error.message : 'Internal error'
|
|
|
|
|
+ )
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 404 for unknown endpoints
|
|
|
|
|
- return createMcpErrorResponse('NOT_FOUND', 'Endpoint not found. Use /tools or /call', 404);
|
|
|
|
|
|
|
+ // Close stream
|
|
|
|
|
+ closeSseStream(controller);
|
|
|
|
|
+ return response;
|
|
|
});
|
|
});
|