338 lines
9.7 KiB
TypeScript

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { ShopifyClient } from './clients/shopify.js';
interface ToolModule {
name: string;
description: string;
inputSchema: {
type: 'object';
properties: Record<string, unknown>;
required?: string[];
};
handler: (args: Record<string, unknown>, client: ShopifyClient) => Promise<unknown>;
}
interface ServerConfig {
name: string;
version: string;
shopifyClient: ShopifyClient;
}
export class ShopifyMCPServer {
private server: Server;
private client: ShopifyClient;
private toolModules: Map<string, () => Promise<ToolModule[]>> = new Map();
constructor(config: ServerConfig) {
this.client = config.shopifyClient;
this.server = new Server(
{
name: config.name,
version: config.version,
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
this.setupToolModules();
this.setupHandlers();
}
/**
* Register lazy-loaded tool modules
*/
private setupToolModules(): void {
// Tool modules will be dynamically imported when needed
this.toolModules.set('products', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/products.js');
return module.default;
});
this.toolModules.set('orders', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/orders.js');
return module.default;
});
this.toolModules.set('customers', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/customers.js');
return module.default;
});
this.toolModules.set('inventory', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/inventory.js');
return module.default;
});
this.toolModules.set('collections', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/collections.js');
return module.default;
});
this.toolModules.set('discounts', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/discounts.js');
return module.default;
});
this.toolModules.set('shipping', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/shipping.js');
return module.default;
});
this.toolModules.set('fulfillments', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/fulfillments.js');
return module.default;
});
this.toolModules.set('themes', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/themes.js');
return module.default;
});
this.toolModules.set('pages', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/pages.js');
return module.default;
});
this.toolModules.set('blogs', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/blogs.js');
return module.default;
});
this.toolModules.set('analytics', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/analytics.js');
return module.default;
});
this.toolModules.set('webhooks', async () => {
// @ts-ignore - Tool modules not created yet (foundation only)
const module = await import('./tools/webhooks.js');
return module.default;
});
}
/**
* Load all tool modules (called on list_tools)
*/
private async loadAllTools(): Promise<ToolModule[]> {
const allTools: ToolModule[] = [];
// Only load modules that exist (we haven't created tool files yet)
// This prevents errors during foundation setup
for (const [moduleName, loader] of this.toolModules.entries()) {
try {
const tools = await loader();
allTools.push(...tools);
} catch (error) {
// Tool module doesn't exist yet - this is expected during foundation setup
console.log(`[Info] Tool module '${moduleName}' not loaded (not created yet)`);
}
}
return allTools;
}
/**
* Setup MCP protocol handlers
*/
private setupHandlers(): void {
// List available tools
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] Listing tools`);
const tools = await this.loadAllTools();
return {
tools: tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
})),
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] Tool call: ${request.params.name}`);
try {
const tools = await this.loadAllTools();
const tool = tools.find(t => t.name === request.params.name);
if (!tool) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: 'TOOL_NOT_FOUND',
message: `Tool '${request.params.name}' not found`,
}),
},
],
isError: true,
};
}
const result = await tool.handler(
request.params.arguments as Record<string, unknown>,
this.client
);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorCode = (error as Error & { code?: string }).code || 'UNKNOWN_ERROR';
console.error(`[${timestamp}] Tool error:`, errorMessage);
return {
content: [
{
type: 'text',
text: JSON.stringify({
error: errorCode,
message: errorMessage,
}),
},
],
isError: true,
};
}
});
// List resources (for UI apps)
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] Listing resources`);
return {
resources: [
{
uri: 'shopify://products',
name: 'Products',
description: 'List all products in the store',
mimeType: 'application/json',
},
{
uri: 'shopify://orders',
name: 'Orders',
description: 'List all orders in the store',
mimeType: 'application/json',
},
{
uri: 'shopify://customers',
name: 'Customers',
description: 'List all customers in the store',
mimeType: 'application/json',
},
{
uri: 'shopify://inventory',
name: 'Inventory',
description: 'View inventory levels across locations',
mimeType: 'application/json',
},
],
};
});
// Read resource (for UI apps)
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] Reading resource: ${request.params.uri}`);
const uri = request.params.uri;
try {
let data: unknown;
if (uri === 'shopify://products') {
data = await this.client.list('/products.json');
} else if (uri === 'shopify://orders') {
data = await this.client.list('/orders.json');
} else if (uri === 'shopify://customers') {
data = await this.client.list('/customers.json');
} else if (uri === 'shopify://inventory') {
data = await this.client.list('/inventory_levels.json');
} else {
throw new Error(`Unknown resource URI: ${uri}`);
}
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(data, null, 2),
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`[${timestamp}] Resource read error:`, errorMessage);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify({
error: 'RESOURCE_READ_ERROR',
message: errorMessage,
}),
},
],
};
}
});
}
/**
* Start the server with stdio transport
*/
async start(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.log('Shopify MCP server running');
}
/**
* Get the underlying MCP server instance (for custom transports)
*/
getServer(): Server {
return this.server;
}
}