338 lines
9.7 KiB
TypeScript
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;
|
|
}
|
|
}
|